package ms.sudoku;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import ms.sudoku.technique.EliminationTechnique;
import ms.sudoku.util.IntSet;

import org.lex.game.sudoku.model.Point;

import common.Difficulty;
import common.Sudokus;

//Description: Stores the current configuration of a puzzle.
public final class PuzzleState implements Cloneable {
	private static final byte _defaultBoxSize = 3;
	public static final byte MaxBoxSize = 3;
	public static final byte MinBoxSize = 3;

	private int boxSize;
	private int gridSize;
	private int[][] grid;
	private Difficulty difficulty = Difficulty.Easy;
	private int filledCells = 0;
	private PuzzleStatus status = PuzzleStatus.Unknown;

	public PuzzleState() {
		this(_defaultBoxSize);
	}

	public PuzzleState(int boxSize) {
		this.boxSize = boxSize;
		this.gridSize = (boxSize * boxSize);
		this.grid = new int[this.gridSize][this.gridSize];

		for (int i = 0; i < gridSize; i++) {
			for (int j = 0; j < gridSize; j++) {
				this.grid[i][j] = Sudokus.EMPTY_NUMBER;
			}
		}
	}

	public int getBoxSize() {
		return boxSize;
	}

	public int getGridSize() {
		return gridSize;
	}

	public Difficulty getDifficulty() {
		return difficulty;
	}

	public void setDifficulty(Difficulty value) {
		difficulty = value;
	}

	public int get(Point cell) {
		return this.get(cell.x, cell.y);
	}

	public int get(int x, int y) {
		return grid[x][y];
	}

	public void set(int x, int y, int newValue) {
		int oldValue = grid[x][y];
		if (oldValue != newValue) {
			status = PuzzleStatus.Unknown;
			filledCells = 0;
			grid[x][y] = newValue;
		}
	}

	public void setCell(Point cell, int value) {
		this.set(cell.x, cell.y, value);
	}

	public PuzzleStatus getStatus() {
		if (status == PuzzleStatus.Unknown)
			status = analyzeSolutionStatus();

		return status;
	}

	public int getFilledCellCount() {
		int count = 0;
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				int number = this.grid[row][col];
				if (false == Sudokus.isEmpty(number)) {
					count++;
				}
			}
		}
		return count;
	}

	public PuzzleState clone() {
		PuzzleState ns = new PuzzleState(this.boxSize);
		ns.gridSize = this.gridSize;
		ns.difficulty = this.difficulty;
		ns.filledCells = this.filledCells;
		ns.status = this.status;
		for (int row = 0; row < gridSize; row++) {
			System.arraycopy(this.grid[row], 0, ns.grid[row], 0, gridSize);
		}
		return ns;
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + boxSize;
		result = prime * result + ((difficulty == null) ? 0 : difficulty.hashCode());
		result = prime * result + filledCells;
		result = prime * result + Arrays.hashCode(grid);
		result = prime * result + gridSize;
		result = prime * result + ((status == null) ? 0 : status.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		PuzzleState other = (PuzzleState) obj;
		if (boxSize != other.boxSize)
			return false;
		if (difficulty != other.difficulty)
			return false;
		if (filledCells != other.filledCells)
			return false;
		if (!Arrays.equals(grid, other.grid))
			return false;
		if (gridSize != other.gridSize)
			return false;
		if (status != other.status)
			return false;
		return true;
	}

	public String toString() {
		StringBuilder buf = new StringBuilder();
		String nl = System.getProperty("line.separator", "\n");
		int gridSize = this.getGridSize();
		buf.append("Sudoku model: ").append(gridSize).append('X').append(gridSize).append(nl);
		for (int row = 0; row < gridSize; row++) {
			for (int col = 0; col < gridSize; col++) {
				int num = this.grid[row][col];
				if (Sudokus.notEmpty(num)) {
					buf.append(' ').append(num + 1).append(' ');
				} else {
					buf.append("[_]");
				}
				buf.append(' ');
			}
			buf.append(nl);
		}
		return buf.toString();
	}

	/**
	 * Determines what numbers are possible in each of the cells in the state.
	 * 
	 * @param techniques
	 *            The techniques to use for this elimination process.
	 * @param usesOfTechnique
	 *            How much each of the techniques was used.
	 * @param onlyOnePass
	 *            Whether only one pass should be made through each technique,
	 *            or whether it should repeat all techniques as long as any
	 *            technique made any changes.
	 * @param earlyExitWhenSoleFound
	 * @param possibleNumbers
	 *            The possible numbers used for this computation.
	 * @return The computed possible numbers.
	 */
	public IntSet[][] computePossibleNumbers(List<EliminationTechnique> techniques,
			Map<EliminationTechnique, Integer> usesOfTechnique, boolean onlyOnePass, boolean earlyExitWhenSoleFound,
			IntSet[][] possibleNumbers) {
		for (int row = 0; row < this.gridSize; row++) {
			for (int col = 0; col < this.gridSize; col++) {
				int num = this.get(row, col);
				boolean empty = Sudokus.isEmpty(num);
				if (empty) {
					possibleNumbers[row][col].addAll();
				} else {
					possibleNumbers[row][col].clear();
				}
			}
		}
		boolean notDone = false;
		do {
			notDone = false;
			for (EliminationTechnique et : techniques) {
				int numberOfChanges = 0;
				boolean exitedEarly = false;
				AtomicInteger numberOfChangesNew = new AtomicInteger();
				AtomicBoolean exitedEarlyNew = new AtomicBoolean();
				notDone |= et
						.execute(this, earlyExitWhenSoleFound, possibleNumbers, numberOfChangesNew, exitedEarlyNew);
				numberOfChanges = numberOfChangesNew.get();
				exitedEarly = exitedEarlyNew.get();
				if (usesOfTechnique != null) {
					Integer uses = usesOfTechnique.get(et);
					usesOfTechnique.put(et, (uses != null) ? numberOfChanges + uses : numberOfChanges);
				}

				if (exitedEarly) {
					notDone = false;
					break;
				}

			}
		} while (notDone && !onlyOnePass);
		return possibleNumbers;
	}

	private PuzzleStatus analyzeSolutionStatus() {
		IntSet numbersUsed = new IntSet(gridSize);
		for (int row = 0; row < gridSize; row++) {
			numbersUsed.clear();
			for (int col = 0; col < gridSize; col++) {
				int num = grid[row][col];
				if (false == Sudokus.isEmpty(num)) {
					if (numbersUsed.contains(num)) {
						return PuzzleStatus.CannotBeSolved;
					}
					numbersUsed.add(num);
				}
			}
		}
		for (int row = 0; row < gridSize; row++) {
			numbersUsed.clear();
			for (int col = 0; col < gridSize; col++) {
				int num = grid[col][row];
				if (false == Sudokus.isEmpty(num)) {
					if (numbersUsed.contains(num)) {
						return PuzzleStatus.CannotBeSolved;
					}
					numbersUsed.add(num);
				}
			}
		}
		for (int boxNumber = 0; boxNumber < gridSize; boxNumber++) {
			numbersUsed.clear();
			int boxStartX = (boxNumber / boxSize) * boxSize;
			for (int x = boxStartX; x < boxStartX + boxSize; x++) {
				int boxStartY = (boxNumber % boxSize) * boxSize;
				for (int y = boxStartY; y < boxStartY + boxSize; y++) {
					int num = grid[x][y];
					if (false == Sudokus.isEmpty(num)) {
						if (numbersUsed.contains(num)) {
							return PuzzleStatus.CannotBeSolved;
						}
						numbersUsed.add(num);
					}
				}
			}
		}
		for (int i = 0; i < gridSize; i++) {
			for (int j = 0; j < gridSize; j++) {
				if (Sudokus.isEmpty(grid[i][j]))
					return PuzzleStatus.InProgress;

			}
		}
		return PuzzleStatus.Solved;
	}

}
